package com.flow.framework.module.call.rpc.interceptor;

import com.flow.framework.common.constant.FrameworkCommonConstant;
import com.flow.framework.common.error.SystemErrorCode;
import com.flow.framework.common.exception.CheckedException;
import com.flow.framework.common.json.JsonArray;
import com.flow.framework.common.json.JsonObject;
import com.flow.framework.common.util.verify.VerifyUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;

import javax.annotation.Nullable;
import java.util.*;

/**
 * 将openFeign GET/DELETE请求中的body转化成标准的http请求
 *
 * @author luoguopiao
 * @version 0.0.1
 * @date 2022/2/19
 */
@Slf4j
public class FeignRequestParamsInterceptor implements RequestInterceptor {

    /**
     * 需要将body数据转化为url查询数据的请求方法
     */
    private static final List<String> CONVERT_BODY_TO_QUERY_METHODS = new ArrayList<String>() {{
        add(HttpMethod.GET.name());
        add(HttpMethod.DELETE.name());
    }};

    /**
     * feign在传递POJO时，即使是GET/DELETE也是使用请求body传递数据
     * 该方法的主要目的是将body转换成请求的查询数据，需要注意，feign默认使用的查询参数拼接方式为feign.CollectionFormat#EXPLODED
     * 这种方式可以避免特殊字符转义带来的业务出错，比如如果使用feign.CollectionFormat#CSV进行处理，此时如果查询参数中出现逗号
     * 则可能造成服务端误将该逗号识别成参数分隔符了，故feign.CollectionFormat#EXPLODED这种方式是最好的，但缺点是这种方式会增加URL的长度
     * 所以特别复杂且参数非常多的查询建议使用PUT请求方式
     *
     * @param template template
     */
    @Override
    public void apply(RequestTemplate template) {
        if (CONVERT_BODY_TO_QUERY_METHODS.contains(template.method())) {
            byte[] body = template.body();
            if (body == null) {
                return;
            }
            String bodyString = new String(body);
            if (VerifyUtil.isEmpty(bodyString)) {
                return;
            }
            Map<String, Collection<String>> queries = new HashMap<>(template.queries());
            try {
                addJsonObjectToQueries(FrameworkCommonConstant.EMPTY_STRING, JsonObject.fromObject(bodyString), queries);
                template.body(FrameworkCommonConstant.EMPTY_STRING);
                template.queries(queries);
            } catch (Exception e) {
                log.error("compose get param error.", e);
                throw new CheckedException(SystemErrorCode.UNEXPECTED_ERROR, "compose get param error", e);
            }
        }
    }

    private void addEntryToQueries(@Nullable String parentKey, Map.Entry<String, Object> entry,
                                   Map<String, Collection<String>> queries) {
        String key = entry.getKey();
        if (!VerifyUtil.isEmpty(parentKey)) {
            key = parentKey + "." + key;
        }
        Object value = entry.getValue();
        if (null == value) {
            queries.put(key, Collections.emptyList());
            return;
        }

        if (value instanceof JsonObject) {
            JsonObject jsonObject = (JsonObject) value;
            addJsonObjectToQueries(key, jsonObject, queries);
        } else if (value instanceof JsonArray) {
            List<String> paramValues = new ArrayList<>();
            JsonArray jsonArray = (JsonArray) value;
            for (Object o : jsonArray) {
                if (o instanceof String) {
                    paramValues.add((String) o);
                } else if (o instanceof JsonObject) {
                    JsonObject jsonObject = (JsonObject) o;
                    addJsonObjectToQueries(key, jsonObject, queries);
                } else {
                    paramValues.add(String.valueOf(o));
                }
            }
            queries.put(key, paramValues);
        } else {
            queries.put(key, Collections.singletonList(String.valueOf(value)));
        }
    }

    private void addJsonObjectToQueries(String parentKey, JsonObject jsonObject, Map<String, Collection<String>> queries) {
        Set<Map.Entry<String, Object>> entries = jsonObject.entrySet();
        for (Map.Entry<String, Object> entry : entries) {
            String key = entry.getKey();
            if (!VerifyUtil.isEmpty(parentKey)) {
                key = parentKey + "." + key;
            }
            Object value = entry.getValue();
            if (null == value) {
                queries.put(key, Collections.emptyList());
                continue;
            }
            if (value instanceof JsonObject || value instanceof JsonArray) {
                addEntryToQueries(parentKey, entry, queries);
            } else {
                queries.put(key, Collections.singletonList(String.valueOf(value)));
            }
        }
    }
}
